/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { AbstractInvokeExpr, ArkAssignStmt, ArkMethod, ArkNewExpr, ArkReturnVoidStmt, ArkThisRef, BasicBlock, ClosureType, DEFAULT_ARK_CLASS_NAME, FunctionType, Local, Stmt } from 'arkanalyzer/lib';
import { ArkClass } from 'arkanalyzer/lib/core/model/ArkClass';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { CheckerUtils, ClassMatcher, Defects, MatcherCallback, MatcherTypes, Rule } from '../../Index';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'ReuseDateInstancesCheck');
const loopInvokeMethods: string[] = ['forEach', 'map', 'flatMap', 'filter', 'reduce', 'every', 'some', 'find'];
const visitedBlockSet: Set<BasicBlock> = new Set();
const processedClasses: Set<ArkClass> = new Set();
const dateSign: string[] = [
    `@built-in/lib.es5.d.ts: Date`
];
const setIntervalSign: string[] = [
    `@ohosSdk/api/@internal/full/global.d.ts: ${DEFAULT_ARK_CLASS_NAME}.setInterval(@built-in/lib.es5.d.ts: Function|string, number, any[])`
];
const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/reuse-date-instances-check.md',
    description: 'Avoid repeated creation of Date objects in loops or frequently called methods.'
};

export class ReuseDateInstancesCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS
    };

    public registerMatchers(): MatcherCallback[] {
        const matchClazzCb: MatcherCallback = {
            matcher: this.clsMatcher,
            callback: this.check
        };
        return [matchClazzCb];
    }

    public check = (target: ArkClass): void => {
        if (processedClasses.has(target)) {
            return;
        }
        processedClasses.add(target);
        for (let method of target.getMethods()) {
            if (method.isGenerated()) {
                continue;
            }
            const firstBlock = method.getBody()?.getCfg().getStartingBlock();
            if (!firstBlock) {
                return;
            }
            this.blockProcess(target, method, firstBlock);
            this.processLoopStmts(target, method);
            this.processStmts(target, method);
        }
    };

    private blockProcess(clazz: ArkClass, method: ArkMethod, block: BasicBlock): void {
        const stmts = block.getStmts();
        if (stmts.length === 0) {
            return;
        }
        if (this.isFirstBlock(method, block)) {
            const succBlocks = block.getSuccessors();
            if (succBlocks.length > 0) {
                this.blockProcess(clazz, method, succBlocks[0]);
            }
            return;
        }
        visitedBlockSet.clear();
        visitedBlockSet.add(block);
        if (this.isLoopBlock(block.getId(), block.getSuccessors())) {
            this.loopBlockStmtsProcess(clazz, stmts);
        }
        const blocks = method.getBody()?.getCfg().getBlocks();
        if (!blocks) {
            return;
        }
        if (block.getId() < 0 || block.getId() > blocks.size) {
            return;
        }
        let nextBlock = null;
        for (let value of blocks) {
            if (value.getId() === (block.getId() + 1)) {
                nextBlock = value;
                break;
            }
        }
        if (!nextBlock) {
            return;
        }
        this.blockProcess(clazz, method, nextBlock);
    }

    private processLoopStmts(clazz: ArkClass, method: ArkMethod): void {
        for (let stmt of method.getBody()?.getCfg().getStmts() ?? []) {
            const invokerExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokerExpr) {
                continue;
            }
            const name = invokerExpr.getMethodSignature().getMethodSubSignature().getMethodName();
            if (!loopInvokeMethods.includes(name)) {
                continue;
            }
            const mtd = this.hasInvokeMethod(clazz, invokerExpr);
            if (!mtd) {
                continue;
            }
            const stmts = mtd.getCfg()?.getStmts();
            if (!stmts) {
                continue;
            }
            this.loopBlockStmtsProcess(clazz, stmts);
        }
    }

    private processStmts(clazz: ArkClass, method: ArkMethod): void {
        const stmts = method.getBody()?.getCfg().getStmts() ?? [];
        for (let stmt of stmts) {
            const invokerExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokerExpr) {
                continue;
            }
            const sign = invokerExpr.getMethodSignature().toString();
            if (!setIntervalSign.includes(sign)) {
                continue;
            }
            const args = invokerExpr.getArgs();
            if (args.length === 0) {
                continue;
            }
            const type = args[0].getType();
            if (!(type instanceof FunctionType)) {
                continue;
            }
            const mtd = method.getDeclaringArkClass().getMethod(type.getMethodSignature());
            if (!mtd) {
                continue;
            }
            const declaringStmts = mtd.getBody()?.getCfg().getStmts() ?? [];
            this.loopBlockStmtsProcess(clazz, declaringStmts);
        }
    }

    private loopBlockStmtsProcess(clazz: ArkClass, stmts: Stmt[], cachedMethods: Set<string> = new Set<string>()): void {
        if (stmts.length === 0) {
            return;
        }
        const methodKey: string = stmts[0].getCfg()?.getDeclaringMethod()?.getSignature().toMapKey();
        if (cachedMethods.has(methodKey)) {
            return;
        }
        cachedMethods.add(methodKey);
        for (let stmt of stmts) {
            if (stmt instanceof ArkAssignStmt) {
                this.arkAssignStmtProcess(stmt);
            }
            const invokerExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokerExpr) {
                continue;
            }
            const targetMethod = clazz.getMethod(invokerExpr.getMethodSignature());
            if (!targetMethod) {
                continue;
            }
            if (targetMethod.isGenerated()) {
                continue;
            }
            const targetStmts = targetMethod.getCfg()?.getStmts();
            if (!targetStmts) {
                continue;
            }
            this.loopBlockStmtsProcess(clazz, targetStmts, cachedMethods);
        }
    }

    private isLoopBlock(currentBlockId: number, successorBlocks: BasicBlock[]): boolean {
        let flag = false;
        for (let succBlock of successorBlocks) {
            if (succBlock.getId() === currentBlockId) {
                flag = true;
                break;
            } else if (this.isLastBlock(succBlock)) {
                continue;
            } else if (visitedBlockSet.has(succBlock)) {
                continue;
            } else {
                visitedBlockSet.add(succBlock);
                if (this.isLoopBlock(currentBlockId, succBlock.getSuccessors())) {
                    flag = true;
                }
            }
        }
        return flag;
    }

    private isFirstBlock(method: ArkMethod, block: BasicBlock): boolean {
        if (block.getPredecessors().length !== 0) {
            return false;
        }
        const stmts = block.getStmts();
        const pCount = method.getParameters().length;
        if (pCount < 0 || pCount > stmts.length) {
            return false;
        }
        const thisRefStmt = stmts[pCount];
        if (thisRefStmt instanceof ArkAssignStmt && thisRefStmt.getRightOp() instanceof ArkThisRef) {
            return true;
        }
        return false;
    }

    private isLastBlock(block: BasicBlock): boolean {
        if (block.getPredecessors().length !== 0) {
            return false;
        }
        const stmts = block.getStmts();
        if (stmts.length === 0) {
            return false;
        }
        const returnStmt = stmts[stmts.length - 1];
        if (returnStmt instanceof ArkAssignStmt || returnStmt instanceof ArkReturnVoidStmt) {
            return true;
        }
        return false;
    }

    private hasInvokeMethod(clazz: ArkClass, rightOp: AbstractInvokeExpr): ArkMethod | null {
        if (rightOp.getArgs().length <= 0) {
            return null;
        }
        const arg0 = rightOp.getArg(0);
        if (!(arg0 instanceof Local)) {
            return null;
        }
        const localType = arg0.getType();
        if (!(localType instanceof FunctionType)) {
            return null;
        }
        const mtd = clazz.getMethod(localType.getMethodSignature());
        if (mtd && !mtd.isGenerated()) {
            return mtd;
        }
        return null;
    }

    private arkAssignStmtProcess(stmt: ArkAssignStmt): void {
        const rightOp = stmt.getRightOp();
        if (!(rightOp instanceof ArkNewExpr)) {
            return;
        }
        const sign = rightOp.getClassType().getClassSignature();
        if (!dateSign.includes(sign.toString())) {
            return;
        }
        this.reportIssue(stmt, rightOp);
    }

    private reportIssue(stmt: Stmt, rightOp: ArkNewExpr): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile();
        const originPosition = stmt.getOperandOriginalPosition(rightOp);
        if (arkFile && originPosition) {
            let lineNum = originPosition.getFirstLine();
            let startColumn = originPosition.getFirstCol();
            let endColunm = originPosition.getLastCol() - 1;
            const filePath = arkFile.getFilePath();
            const originText = stmt.getOriginalText();
            if (!originText || originText.length === 0 || !originText.includes('new')) {
                return;
            }
            let defects = new Defects(lineNum, startColumn, endColunm, this.metaData.description, severity, this.rule.ruleId,
                filePath, this.metaData.ruleDocPath, true, false, false);
            this.issues.push(new IssueReport(defects, undefined));
        }
    }
}